package org.newdawn.slick;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;

import javax.swing.JScrollPane;
import javax.swing.JTextPane;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Cursor;
import org.lwjgl.input.Mouse;
import org.lwjgl.openal.AL;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.PixelFormat;
import org.newdawn.slick.openal.SoundStore;
import org.newdawn.slick.opengl.CursorLoader;
import org.newdawn.slick.opengl.ImageData;
import org.newdawn.slick.opengl.ImageIOImageData;
import org.newdawn.slick.opengl.InternalTextureLoader;
import org.newdawn.slick.opengl.LoadableImageData;
import org.newdawn.slick.opengl.TGAImageData;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;

/**
 * A game container that will display the game as an stand alone application.
 * 
 * @author kevin
 */
public class AppGameContainer extends GameContainer {
	static {
		AccessController.doPrivileged(new PrivilegedAction() {
			public Object run() {
				try {
					Display.getDisplayMode();
				} catch (Exception e) {
					Log.error(e);
				}
				return null;
			}
		});
	}

	/** The original display mode before we tampered with things */
	protected DisplayMode originalDisplayMode;

	/** The display mode we're going to try and use */
	protected DisplayMode targetDisplayMode;

	/** True if we should update the game only when the display is visible */
	protected boolean updateOnlyOnVisible = true;

	/** Alpha background supported */
	protected boolean alphaSupport = true;

	/**
	 * Create a new container wrapping a game
	 * 
	 * @param game
	 *            The game to be wrapped
	 * @throws SlickException
	 *             Indicates a failure to initialise the display
	 */
	public AppGameContainer(Game game) throws SlickException {
		this(game, 640, 480, false);
	}

	/**
	 * Create a new container wrapping a game
	 * 
	 * @param game
	 *            The game to be wrapped
	 * @param width
	 *            The width of the display required
	 * @param height
	 *            The height of the display required
	 * @param fullscreen
	 *            True if we want fullscreen mode
	 * @throws SlickException
	 *             Indicates a failure to initialise the display
	 */
	public AppGameContainer(Game game, int width, int height, boolean fullscreen)
			throws SlickException {
		super(game);

		originalDisplayMode = Display.getDisplayMode();

		setDisplayMode(width, height, fullscreen);
	}

	/**
	 * Check if the display created supported alpha in the back buffer
	 * 
	 * @return True if the back buffer supported alpha
	 */
	public boolean supportsAlphaInBackBuffer() {
		return alphaSupport;
	}

	/**
	 * Set the title of the window
	 * 
	 * @param title
	 *            The title to set on the window
	 */
	public void setTitle(String title) {
		Display.setTitle(title);
	}

	/**
	 * Set the display mode to be used
	 * 
	 * @param width
	 *            The width of the display required
	 * @param height
	 *            The height of the display required
	 * @param fullscreen
	 *            True if we want fullscreen mode
	 * @throws SlickException
	 *             Indicates a failure to initialise the display
	 */
	public void setDisplayMode(int width, int height, boolean fullscreen)
			throws SlickException {
		if ((this.width == width) && (this.height == height)
				&& (isFullscreen() == fullscreen)) {
			return;
		}

		try {
			targetDisplayMode = null;
			if (fullscreen) {
				DisplayMode[] modes = Display.getAvailableDisplayModes();
				int freq = 0;

				for (int i = 0; i < modes.length; i++) {
					DisplayMode current = modes[i];

					if ((current.getWidth() == width)
							&& (current.getHeight() == height)) {
						if ((targetDisplayMode == null)
								|| (current.getFrequency() >= freq)) {
							if ((targetDisplayMode == null)
									|| (current.getBitsPerPixel() > targetDisplayMode
											.getBitsPerPixel())) {
								targetDisplayMode = current;
								freq = targetDisplayMode.getFrequency();
							}
						}

						// if we've found a match for bpp and frequence against
						// the
						// original display mode then it's probably best to go
						// for this one
						// since it's most likely compatible with the monitor
						if ((current.getBitsPerPixel() == originalDisplayMode
								.getBitsPerPixel())
								&& (current.getFrequency() == originalDisplayMode
										.getFrequency())) {
							targetDisplayMode = current;
							break;
						}
					}
				}
			} else {
				targetDisplayMode = new DisplayMode(width, height);
			}

			if (targetDisplayMode == null) {
				throw new SlickException("Failed to find value mode: " + width
						+ "x" + height + " fs=" + fullscreen);
			}

			this.width = width;
			this.height = height;

			Display.setDisplayMode(targetDisplayMode);
			Display.setFullscreen(fullscreen);

			if (Display.isCreated()) {
				initGL();
				enterOrtho();
			}

			if (targetDisplayMode.getBitsPerPixel() == 16) {
				InternalTextureLoader.get().set16BitMode();
			}
		} catch (LWJGLException e) {
			throw new SlickException("Unable to setup mode " + width + "x"
					+ height + " fullscreen=" + fullscreen, e);
		}

		getDelta();
	}

	/**
	 * Check if the display is in fullscreen mode
	 * 
	 * @return True if the display is in fullscreen mode
	 */
	public boolean isFullscreen() {
		return Display.isFullscreen();
	}

	/**
	 * Indicate whether we want to be in fullscreen mode. Note that the current
	 * display mode must be valid as a fullscreen mode for this to work
	 * 
	 * @param fullscreen
	 *            True if we want to be in fullscreen mode
	 * @throws SlickException
	 *             Indicates we failed to change the display mode
	 */
	public void setFullscreen(boolean fullscreen) throws SlickException {
		if (isFullscreen() == fullscreen) {
			return;
		}

		if (!fullscreen) {
			try {
				Display.setFullscreen(fullscreen);
			} catch (LWJGLException e) {
				throw new SlickException("Unable to set fullscreen="
						+ fullscreen, e);
			}
		} else {
			setDisplayMode(width, height, fullscreen);
		}
		getDelta();
	}

	/**
	 * @see org.newdawn.slick.GameContainer#setMouseCursor(java.lang.String,
	 *      int, int)
	 */
	public void setMouseCursor(String ref, int hotSpotX, int hotSpotY)
			throws SlickException {
		try {
			Cursor cursor = CursorLoader.get().getCursor(ref, hotSpotX,
					hotSpotY);
			Mouse.setNativeCursor(cursor);
		} catch (Exception e) {
			Log.error("Failed to load and apply cursor.", e);
		}
	}

	/**
	 * @see org.newdawn.slick.GameContainer#setMouseCursor(org.newdawn.slick.opengl.ImageData,
	 *      int, int)
	 */
	public void setMouseCursor(ImageData data, int hotSpotX, int hotSpotY)
			throws SlickException {
		try {
			Cursor cursor = CursorLoader.get().getCursor(data, hotSpotX,
					hotSpotY);
			Mouse.setNativeCursor(cursor);
		} catch (Exception e) {
			Log.error("Failed to load and apply cursor.", e);
		}
	}

	/**
	 * @see org.newdawn.slick.GameContainer#setMouseCursor(org.lwjgl.input.Cursor,
	 *      int, int)
	 */
	public void setMouseCursor(Cursor cursor, int hotSpotX, int hotSpotY)
			throws SlickException {
		try {
			Mouse.setNativeCursor(cursor);
		} catch (Exception e) {
			Log.error("Failed to load and apply cursor.", e);
		}
	}

	/**
	 * @see org.newdawn.slick.GameContainer#setMouseCursor(org.newdawn.slick.Image,
	 *      int, int)
	 */
	public void setMouseCursor(Image image, int hotSpotX, int hotSpotY)
			throws SlickException {
		try {
			ByteBuffer buffer = ByteBuffer.allocate(image.getWidth()
					* image.getHeight() * 4);
			image.getGraphics().getArea(0, 0, image.getWidth(),
					image.getHeight(), buffer);

			Cursor cursor = CursorLoader.get().getCursor(buffer, hotSpotX,
					hotSpotY, image.getWidth(), image.getHeight());
			Mouse.setNativeCursor(cursor);
		} catch (Exception e) {
			Log.error("Failed to load and apply cursor.", e);
		}
	}

	/**
	 * @see org.newdawn.slick.GameContainer#reinit()
	 */
	public void reinit() throws SlickException {
		InternalTextureLoader.get().clear();
		SoundStore.get().clear();
		initSystem();
		enterOrtho();

		try {
			game.init(this);
		} catch (SlickException e) {
			Log.error(e);
			running = false;
		}
	}

	/**
	 * Start running the game
	 * 
	 * @throws SlickException
	 *             Indicates a failure to initialise the system
	 */
	public void start() throws SlickException {
		try {
			if (targetDisplayMode == null) {
				setDisplayMode(640, 480, false);
			}

			Display.setTitle(game.getTitle());

			Log.info("LWJGL Version: " + Sys.getVersion());
			Log.info("OriginalDisplayMode: " + originalDisplayMode);
			Log.info("TargetDisplayMode: " + targetDisplayMode);

			AccessController.doPrivileged(new PrivilegedAction() {
				public Object run() {
					int sample=8;
					PixelFormat format;
					while (!Display.isCreated()) {
						try {
							format = new PixelFormat(8, 8, 0,sample);
							if (SHARED_DRAWABLE == null) {
								Display.create(format);
							} else {
								Display.create(format, SHARED_DRAWABLE);
							}
							//System.out.println("Samples:"+sample);
						} catch (Exception e) {
							if(sample%2!=0)	
								sample--;
							else 
								sample-=2;
							if(sample<0) break;
							//Log.error(e);
						}
					}

					if (!Display.isCreated()) {
						alphaSupport = false;
						Display.destroy();
						try {
							if (SHARED_DRAWABLE == null) {
								Display.create();
							} else {
								Display.create(new PixelFormat(),SHARED_DRAWABLE);
							}
						} catch (Exception x) {
							Log.error(x);
						}
					}
					return null;
				}
			});

			if (!Display.isCreated()) {
				throw new SlickException(
						"Failed to initialise the LWJGL display");
			}

			initSystem();
			enterOrtho();

			try {
				getInput().initControllers();
			} catch (SlickException e) {
				Log.info("Controllers not available");
			} catch (Throwable e) {
				Log.info("Controllers not available");
			}

			try {
				game.init(this);
			} catch (SlickException e) {
				Log.error(e);
				running = false;
			}

			getDelta();
			while (running()) {
				int delta = getDelta();
				if(delta>60)delta = 60;

				if (!Display.isVisible() && updateOnlyOnVisible) {
					try {
						Thread.sleep(100);
					} catch (Exception e) {
					}
				} else {
					try {
						updateAndRender(delta);
					} catch (SlickException e) {
						Log.error(e);
						showError(e);
						running = false;
						break;
					}
				}

				updateFPS();

				Display.update();

				if (Display.isCloseRequested()) {
					if (game.closeRequested()) {
						running = false;
					}
				}
			}
		} finally {
			AL.destroy();
			Display.destroy();
		}

		if (forceExit) {
			System.exit(0);
		}
	}

	/**
	 * @see org.newdawn.slick.GameContainer#setUpdateOnlyWhenVisible(boolean)
	 */
	public void setUpdateOnlyWhenVisible(boolean updateOnlyWhenVisible) {
		updateOnlyOnVisible = updateOnlyWhenVisible;
	}

	/**
	 * @see org.newdawn.slick.GameContainer#isUpdatingOnlyWhenVisible()
	 */
	public boolean isUpdatingOnlyWhenVisible() {
		return updateOnlyOnVisible;
	}

	/**
	 * @see org.newdawn.slick.GameContainer#setIcon(java.lang.String)
	 */
	public void setIcon(String ref) throws SlickException {
		setIcons(new String[] { ref });
	}

	/**
	 * @see org.newdawn.slick.GameContainer#setMouseGrabbed(boolean)
	 */
	public void setMouseGrabbed(boolean grabbed) {
		Mouse.setGrabbed(grabbed);
	}

	/**
	 * @see org.newdawn.slick.GameContainer#hasFocus()
	 */
	public boolean hasFocus() {
		// hmm, not really the right thing, talk to the LWJGL guys
		return Display.isVisible();
	}

	/**
	 * @see org.newdawn.slick.GameContainer#getScreenHeight()
	 */
	public int getScreenHeight() {
		return originalDisplayMode.getHeight();
	}

	/**
	 * @see org.newdawn.slick.GameContainer#getScreenWidth()
	 */
	public int getScreenWidth() {
		return originalDisplayMode.getWidth();
	}

	/**
	 * Destroy the app game container
	 */
	public void destroy() {
		Display.destroy();
		AL.destroy();
	}

	/**
	 * A null stream to clear out those horrid errors
	 * 
	 * @author kevin
	 */
	private class NullOutputStream extends OutputStream {
		/**
		 * @see java.io.OutputStream#write(int)
		 */
		public void write(int b) throws IOException {
			// null implemetnation
		}

	}

	/**
	 * @see org.newdawn.slick.GameContainer#setIcons(java.lang.String[])
	 */
	public void setIcons(String[] refs) throws SlickException {
		ByteBuffer[] bufs = new ByteBuffer[refs.length];
		for (int i = 0; i < refs.length; i++) {
			LoadableImageData data;
			boolean flip = true;

			if (refs[i].endsWith(".tga")) {
				data = new TGAImageData();
			} else {
				flip = false;
				data = new ImageIOImageData();
			}

			try {
				bufs[i] = data.loadImage(ResourceLoader
						.getResourceAsStream(refs[i]), flip, false, null);
			} catch (Exception e) {
				Log.error(e);
				throw new SlickException("Failed to set the icon");
			}
		}

		Display.setIcon(bufs);
	}

	/**
	 * @see org.newdawn.slick.GameContainer#setDefaultMouseCursor()
	 */
	public void setDefaultMouseCursor() {
		try {
			Mouse.setNativeCursor(null);
		} catch (LWJGLException e) {
			Log.error("Failed to reset mouse cursor", e);
		}
	}
	public void showError(Exception e){
		StringWriter w = new StringWriter();
		PrintWriter out = new PrintWriter(w);
		e.printStackTrace(out);
		JTextPane txt = new JTextPane();
		txt.setText(w.toString());
		javax.swing.JOptionPane.showMessageDialog(null,new JScrollPane(txt),e.getMessage(),javax.swing.JOptionPane.ERROR_MESSAGE);	
	}
}
